package com.firesyde.gwt.persistence.hibernate;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;

import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.slf4j.Logger;

import com.firesyde.gwt.persistence.IDataManager;
import com.firesyde.gwt.persistence.client.StorableDAO;
import com.google.inject.Inject;

public class HibernateDataManager implements
		IDataManager {

	@Inject
	Configuration config;
	@Inject
	Session session;
	@Inject
	Logger logger;

	//cache bean information, used for doing cleanups to replace non-GWT-serializable types
	//with serializable ones
	private HashMap<Class, BeanInfo> beanInfoCache = new HashMap<Class, BeanInfo>(); 
	
	//map of classes to key/value pairs representing the bean property name and destination type
	//uses beanutils for conversion
	private HashMap<Class, HashMap<String, Class>> propertyConversions = new HashMap<Class, HashMap<String, Class>>();
	
//	String tableName = null;

	public void close() {
		session.clear();
	}

	//TODO
	public boolean exists(String key) {
		// session.lo
		return false;
	}

	//TODO return list<t>
	public <T extends StorableDAO> T fetch(String[] keys, Object[] values, Class<T> fetchClass) {
		//done before every fetch type call
		//XXX TODO: optimize, do this once in one place?
		if(!beanInfoCache.containsKey(fetchClass)) {
			getBeanInfo(fetchClass);
		}
		
		StringBuffer queryString = new StringBuffer("from " + fetchClass.getSimpleName());

		//create the query string
		if (keys.length > 0) {
			queryString.append(" where ");
			for (int i = 0; i < keys.length; i++) {
				String key = keys[i];
				queryString.append(key + " = ?");
				if (i < keys.length - 1)
					queryString.append(", ");
			}
		}
		
		logger.info("Executing "+queryString);
		
		//create the query
		Query query = session.createQuery(queryString.toString());
		
		
		
		//check parameters
		for(String param : query.getNamedParameters()) {
			logger.info("Param "+param);
		}
		
		//set the query values
		if (values.length > 0) {
			for (int i = 0; i < values.length; i++) {
				
				Object value = values[i];
				logger.info("Setting value "+(i)+" to "+value);
				query.setParameter(i, value);
			}
		}
		
		List<T> list = query.list();

		if (list.size() > 0) {
			T item = list.get(0);
			fixPropertyTypes(fetchClass, item);
			return item;
		}
		else
			return null;
	}

	private <T extends StorableDAO> void getBeanInfo(Class<T> fetchClass) {
		
		try {
			BeanInfo beanInfo = Introspector.getBeanInfo(fetchClass);
			beanInfoCache.put(fetchClass, beanInfo);
			
			HashMap<String, Class> conversionMap = new HashMap<String, Class>();
			
			for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
				
				//if it's a java.util.Date, Hibernate will replace it with a subclass like java.sql.Date,
				//and we need to convert back.
				if(pd.getPropertyType().equals(java.util.Date.class)) {
					conversionMap.put(pd.getName(), java.util.Date.class);
				}
			}
			
			propertyConversions.put(fetchClass, conversionMap);
			
		} catch (IntrospectionException e) {
			logger.error("Error introspecting beaninfo: "
					+ e.getMessage());
			e.printStackTrace();
		}
	}
	
	public <T extends StorableDAO> List<T> fetchAll(Class<T> fetchClass) {
		if(!beanInfoCache.containsKey(fetchClass)) {
			getBeanInfo(fetchClass);
		}
		
		BeanInfo beanInfo = beanInfoCache.get(fetchClass);
		
		logger.info("Fetching all...");

		String queryString = "from " + fetchClass.getSimpleName();

		session.beginTransaction();

		logger.info("Retrieving list using " + queryString);

		List<T> result = session.createQuery(queryString).list();

		for(T item : result) {
			
			logger.info("Got "+item.toString());
			
			fixPropertyTypes(fetchClass, item);
			
//			fixSQLDate(item, fetchClass);
		}
		
		session.getTransaction().commit();

		return result;
	}

	private <T> void fixPropertyTypes(Class<T> fetchClass, T item) {
		HashMap<String, Class> propertyConversionsForClass = propertyConversions.get(fetchClass);
		
//		if(propertyConversionsForClass!=null)
		
		//fix up every property that needs conversion
		for(Entry<String, Class> propertyConversion : propertyConversionsForClass.entrySet()) {
			String propName = propertyConversion.getKey();
			logger.info("Converting "+propName+" to "+propertyConversion.getValue().getCanonicalName());
			try {
				//get the property, convert it, set it back. All in one line. Take that, Perl!
				PropertyUtils.setProperty(item, propName, ConvertUtils.convert(PropertyUtils.getProperty(item, propName), propertyConversion.getValue()));
			} catch (Exception e) {
				logger.error("Error converting property "+propertyConversion.getKey()+" to "+propertyConversion.getValue().getCanonicalName()+": "+e.getMessage());
				e.printStackTrace();
			}
		}
	}

	public <T extends StorableDAO> void store(T record) {

		
		Transaction trans = session.beginTransaction();

		session.save(record);

		trans.commit();
	}

}
